// this is "/atnf/atcaSchedule"
define([ "dojo/_base/lang", "./base", "dojo/_base/xhr", "./source" ],
       function(lang, atnfLibrary, xhr, atnfSource) {

  var rObj = {};

  // This file's error parameters.
  var ERR = {
    file: 'atcaSchedule'
  };

  // Some constants.
  rObj.LSB = -1;
  rObj.USB = 1;

  // Some frequency constraints for the CABB bands.
  rObj.bands = [
    {
      'lowFreq': 700,
      'highFreq': 3903,
      'bandName': '16cm'
    },
    {
      'lowFreq': 3904,
      'highFreq': 11952,
      'bandName': '4cm'
    },
    {
      'lowFreq': 15000,
      'highFreq': 26000,
      'bandName': '15mm'
    },
    {
      'lowFreq': 29000,
      'highFreq': 51000,
      'bandName': '7mm'
    },
    {
      'lowFreq': 85000,
      'highFreq': 105000,
      'bandName': '3mm'
    }
  ];


  /**
   * The frequency configuration object will allow us to
   * abstract the master/slave idea without having to handle
   * special cases too much.
   * @param {object} constructor An object that specifies the
   *                             frequency configuration.
   */
  rObj.freqConfig = function(constructor) {

    var that = {};

    // A private object that contains our frequency
    // configuration specification.
    var details = {
      // There are two frequency bands (IFs).
      IF: [],
      freqConfig: 'Master1',
      legal: false
    };

    for (var i = 1; i <= 2; i++) {
      if (typeof constructor['Freq-' + i] !== 'undefined' &&
	  typeof constructor['ChnBW-' + i] !== 'undefined') {
	// We have a continuum band specification.
	details.IF[i] = {
	  'centreFreq': constructor['Freq-' + i],
	  'continuumBandwidth': 2048,
	  'continuumChannelWidth': constructor['ChnBW-' + i],
	  'continuumNChannels': 2048 / constructor['ChnBW-' + i],
	  'zoomChannels': []
	};
	if (typeof constructor['Zoom1-' + i] !== 'undefined') {
	  // There is at least one zoom band configured here.
	  for (var j = 1; j <= 16; j++) {
	    if (typeof constructor['Zoom' + j + '-' + i]
		!== 'undefined') {
	      details.IF[i].zoomChannels[j] =
		constructor['Zoom' + j + '-' + i];
	    }
	  }
	}
      }
    }

    // Our private methods.
    /**
     * Determine which band a particular frequency is in.
     * @param {number} f The frequency to check.
     */
    var inBand = function(f) {
      try {
	if (typeof f === 'undefined' ||
	    !atnfLibrary.isNumeric(f)) {
	  throw(new TypeError('Argument must be a number'));
	}
      } catch (x) {
	atnfLibrary.catchError(ERR, ['inBand', x]);
	return undefined;
      }

      for (var i = 0; i < rObj.bands.length; i++) {
	if (f >= rObj.bands[i].lowFreq &&
	    f <= rObj.bands[i].highFreq) {
	  return rObj.bands[i].bandName;
	}
      }

      return undefined;
    };

    /**
     * Determine some basic properties of this frequency
     * configuration, and figure out whether it is legal.
     */
    var properties = function() {
      // Check which band each IF centre frequency would be in.
      var tBand = [];
      for (var i = 1; i <= 2; i++) {
	tBand[i] = inBand(details.IF[i].centreFreq);
      }
      // Only frequencies in a single band can be observed
      // simultaneously.
      if (tBand[1] !== tBand[2] ||
	  tBand[1] === undefined ||
	  tBand[2] === undefined) {
	// Something isn't right here.
	details.legal = false;
	details.problem = 'The specified continuum frequencies ' +
	  'are in different bands; ';
	for (i = 1; i <= 2; i++) {
	  if (i === 2) {
	    details.problem += ', while ';
	  }
	  details.problem += 'IF' + i + ' is ';
	  if (tBand[i] !== 'undefined') {
	    details.problem += 'in the ' + tBand[i] + ' band';
	  } else {
	    details.problem += ' not in a valid frequency range';
	  }
	}
	return;
      }
      // There is a limit to the separation of the centre
      // frequencies.
      if (Math.abs(details.IF[1].centreFreq -
		   details.IF[2].centreFreq) > 6000) {
	// The frequencies are too far apart.
	details.legal = false;
	details.problem = 'The specified continuum frequencies ' +
	  'are separated by more than the maximum allowed ' +
	  'separation of 6000 MHz';
	return;
      }
      // Determine the sideband.
      if (tBand[1] === '7mm') {
	// In the 7mm band, both the centre frequencies must
	// be below 40 GHz, or above 41 GHz.
	if (!((details.IF[1].centreFreq < 40000 &&
	       details.IF[2].centreFreq < 40000) ||
	      (details.IF[1].centreFreq > 41000 &&
	       details.IF[2].centreFreq > 41000))) {
	  // Not valid.
	  details.legal = false;
	  details.problem = 'In the 7mm band, the continuum ' +
	    'centre frequencies must be either both below ' +
	    '40000 MHz, or both above 41000 MHz';
	  return;
	} else {
	  if (details.IF[1].centreFreq < 40000 &&
	      details.IF[2].centreFreq < 40000) {
	    details.sideband = rObj.LSB;
	  } else {
	    details.sideband = rObj.USB;
	  }
	}
      } else if (tBand[1] === '3mm') {
	// In the 3mm band, we have a complicated set of
	// conditions to satisfy.
	var loFreq = details.IF[1].centreFreq;
	var hiFreq = details.IF[2].centreFreq;
	if (loFreq > hiFreq) {
	  loFreq = details.IF[2].centreFreq;
	  hiFreq = details.IF[1].centreFreq;
	}
	if (hiFreq > 100600 && loFreq > 97800) {
	  details.sideband = rObj.USB;
	} else if (loFreq < 97800 && hiFreq > 100600) {
	  details.sideband = rObj.LSB;
	} else if (loFreq < 97800 && hiFreq < 100600) {
	  details.sideband = rObj.LSB;
	} else if (hiFreq < 100600 && loFreq > 97800) {
	  details.sideband = rObj.USB; // NOT CORRECT!
	}
      } else if (tBand[1] === '16cm' || tBand[1] === '15mm') {
	details.sideband = rObj.LSB;
      } else if (tBand[1] === '4cm') {
	details.sideband = rObj.USB;
      }

      // If we get here we have a legal configuration.
      details.legal = true;
      details.bandName = tBand[1];
    };
    // And run this on the newly constructed configuration.
    properties();

    /**
     * Calculate the low and high frequencies of a zoom
     * channel, given the centre frequency of the continuum
     * IF, the continuum channel width, and the zoom channel
     * number.
     * @param {number} cFreq The continuum IF centre frequency (MHz).
     * @param {number} cWidth The continuum IF channel width (MHz).
     * @param {number} zChan The zoom channel number.
     */
    var calcZoomFreq = function(cFreq, cWidth, zChan) {

    };

    /**
     * Summarise our zoom configuration in terms of
     * consolidated bands, bandwidths and number of
     * channels.
     */
    var makeSummary = function() {
      var summary = []; // One for each IF.
      for (var i = 1; i <= 2; i++) {

      }

    };

    // Our public methods.
    /**
     * Return a copy of our specification.
     */
    that.details = function() {
      return lang.clone(details);
    };

    /**
     * Compare a passed object to see whether it is identical
     * to our contents.
     * @param {object} a Object to compare to. It can either
     *                   be an object that comes back from
     *                   some other freqConfig object's
     *                   details method, or a freqConfig object.
     */
    that.compare = function(a) {
      // Check if this object is actually a freqConfig object.
      if (typeof a === 'undefined') {
	// Nothing was passed to us.
	return false;
      }

      var b;
      if (typeof a.details !== 'undefined' &&
	  lang.isFunction(a.details)) {
	b = a.details();
      } else {
	b = lang.clone(a);
      }

      // Now do the comparison.

    };

    /**
     * Check whether we are a master configuration for
     * the specified index.
     * @param {number} a The index of the master configuration
     *                   to check against.
     */
    that.isMaster = function(a) {
      try {
	if (!atnfLibrary.isNumeric(a)) {
	  throw(new TypeError('Argument must be a Number'));
	}
      } catch (x) {
	atnfLibrary.catchError.apply(ERR, ['isMaster', x]);
      }

      if (details.freqConfig === 'Master' + a) {
	return true;
      }

      return false;
    };

    return that;
  };

  rObj.scheduleScan = function(constructor) {

    var that = {};

    // There should be a source for this scan.
    if (typeof constructor.source !== 'undefined' &&
	atnfLibrary.isSource(constructor.source)) {
      that.source = constructor.source;
    }

    // And now a set of frequencies.
    if (typeof constructor.freqConfig !== 'undefined') {
      that.freqConfig = rObj.freqConfig(constructor.freqConfig);
    }

    // Any ancillary data.
    var translations = {
      'Project': 'projectCode',
      'Observer': 'observer',
      'ScanLength': 'scanLength',
      'TimeCode': 'timeCode',
      'Time': 'time',
      'ScanType': 'scanType',
      'Date': 'date',
      'CalCode': 'calCode'
    };
    if (typeof constructor.all !== 'undefined') {
      for (var t in translations) {
	if (translations.hasOwnProperty(t)) {
	  if (typeof constructor.all[t] !== 'undefined') {
	    that[translations[t]] = constructor.all[t];
	  } else {
	    that[translations[t]] = '';
	  }
	}
      }
    }

    return that;
  };

  rObj.scheduleHandler = function(constructor) {
    /**
     * The object we return to our caller.
     */
    var that = {};

    /**
     * The data for the schedule.
     * @type {Object}
     */
    var schedData = {};

    /**
     * The schedule that should be reference by default.
     * @type {string}
     */
    var defaultSchedule = '';

    /**
     * The constructor needs to be an object.
     * @type {object}
     */
    constructor = constructor || {};

    // Our methods follow.

    // Our private methods.
    /**
     * The name of the server-side script.
     * @type {String}
     */
    var sScript = (typeof constructor.script !== 'undefined') ?
      constructor.script : '/cgi-bin/cabbsched_json';

    var comms = function(spec) {
      var loadPromise = xhr.get({
	  url: sScript,
	  sync: false,
	  handleAs: 'json',
	  content: spec
      });

      return loadPromise;
    };

    /**
     * Set the default schedule for all future calls.
     * @param {string} filename The filename to use.
     */
    var setDefaultSchedule = function(filename) {
      if (typeof filename !== 'undefined' &&
	  lang.isString(filename) &&
	  typeof schedData[filename] !== 'undefined') {
	defaultSchedule = filename;
      }
    };

    /**
     * Interpret and construct the schedule.
     * @param {object} data The JSON data coming from the server.
     */
    var interpret = function(data) {
      // The collection of scans.
      var ourScans = [];

      if (typeof data === 'undefined' ||
	  typeof data.scans === 'undefined' ||
	  data.scans.length === 0) {
	// The load failed.
	return undefined;
      }

      // Go through each of the scans we got back.
      for (var i = 0; i < data.scans.length; i++) {
	// Make the source.
	var tSource = null;
	if (typeof data.scans[i].Epoch !== 'undefined' &&
	    data.scans[i].Epoch === 'J2000') {
	  if (typeof data.scans[i].Source !== 'undefined' &&
	      lang.isString(data.scans[i].Source) &&
	      typeof data.scans[i].RA !== 'undefined' &&
	      lang.isString(data.scans[i].RA) &&
	      typeof data.scans[i].Dec !== 'undefined' &&
	      lang.isString(data.scans[i].Dec)) {
	    tSource = atnfSource.new({
	      'name': data.scans[i].Source,
	      'coordinate': {
		'rightAscension': {
		  'value': atnfLibrary.hexa2turns(data.scans[i].RA, {
		    'units': 'hours'
		    }),
		  'units': 'turns'
		},
		'declination': data.scans[i].Dec,
		'frame': data.scans[i].Epoch
	      }
	    });
	  }
	}
	if (tSource !== null) {
	  ourScans.push(rObj.scheduleScan({
	    'source': tSource,
	    'freqConfig': {
	      'ChnBW-1': data.scans[i]['ChnBW-1'],
	      'ChnBW-2': data.scans[i]['ChnBW-2'],
	      'Freq-1': data.scans[i]['Freq-1'],
	      'Freq-2': data.scans[i]['Freq-2']
	    },
	    'all': data.scans[i]
	  }));

	}
      }

      return ourScans;
    };

    // Our public methods.
    /**
     * Load a schedule.
     * @param {String} filename The name of the schedule to load.
     * @param {object} options Options regarding the loading.
     */
    that.load = function(filename, options) {
      // We must have a string for the filename.
      try {
	if (typeof filename === 'undefined' ||
	    !lang.isString(filename)) {
	  throw(new TypeError('Must supply a filename String.'));
	}
      } catch (x) {
	atnfLibrary.catchError.apply(ERR, ['load', x]);
	return undefined;
      }

      // Check if we already have the information for this
      // schedule.
      if (typeof schedData[filename] !== 'undefined' &&
	  (typeof options === 'undefined' ||
	   typeof options.forceReload === 'undefined' ||
	   !options.forceReload)) {
	setDefaultSchedule(filename);
	// Return our object immediately.
	return that;
      }

      // Otherwise we load the schedule.
      return comms({
	'cmd': 'load',
	'file': filename
      }).then(function(data, ioargs) {
	try {
	  if (typeof data === 'undefined' ||
	      typeof data.filename === 'undefined' ||
	      !lang.isString(data.filename)) {
	    throw(new Error('Load of schedule file failed.'));
	  } else if (typeof data !== 'undefined' &&
		     typeof data.filename !== 'undefined' &&
		     lang.isString(data.filename) &&
		     typeof data.scans !== 'undefined' &&
		     data.scans.length === 0) {
	    // We got a return, but no scans were found.
	    throw(new Error('The file ' + data.filename +
			    ' contains no valid data.'));
	  }
	} catch (x) {
	  atnfLibrary.catchError.apply(ERR, ['load', x]);
	  // We return undefined if the load did not work.
	  return undefined;
	}

	schedData[data.filename] = {
	  'scans': interpret(data)
	};

	if (schedData[data.filename].scans !== undefined) {
	  setDefaultSchedule(data.filename);
	  return that;
	} else {
	  return undefined;
	}
      });
    };

    /**
     * Set a default schedule to use.
     * @param {string} filename The filename to use as default.
     */
    that.setDefaultSchedule = function(filename) {
      // Use the private method.
      setDefaultSchedule(filename);

      // Return ourselves for method chaining.
      return that;
    };

    /**
     * Return the number of scans in this schedule.
     * @param {string} file An optional string which overrides the
     *                      current default schedule file.
     */
    that.numScans = function(file) {
      if (typeof file === 'undefined') {
	return schedData[defaultSchedule].scans.length;
      }

      try {
	if (lang.isString(file) &&
	    typeof schedData[file] === 'undefined') {
	  throw(new Error('Unrecognised schedule file.'));
	}
      } catch (x) {
	atnfLibrary.catchError.apply(ERR, ['numScans', x]);
	return 0;
      }

      return schedData[file].scans.length;
    };

    /**
     * Return a single scan.
     * @param {number} n The scan number to return.
     * @param {string} file An optional string which overrides the
     *                      current default schedule file.
     */
    that.getScan = function(n, file) {
      var locFile = defaultSchedule;

      try {
	if (typeof file !== 'undefined' &&
	    lang.isString(file)) {
	  if (typeof schedData[file] === 'undefined') {
	    throw(new Error('Unrecognised schedule file.'));
	  } else {
	    locFile = file;
	  }
	}
      } catch (x) {
	atnfLibrary.catchError.apply(ERR, ['getScan', x]);
	return undefined;
      }

      try {
	if (n >= schedData[locFile].scans.length) {
	  throw(new RangeError('Scan ' + n + ' not within range.'));
	}
      } catch (x) {
	atnfLibrary.catchError.apply(ERR, ['getScan', x]);
	return undefined;
      }

      return lang.clone(schedData[locFile].scans[n]);
    };

    that.list = function(pattern) {
      var opt = {
	'cmd': 'ls'
      };
      if (typeof pattern !== undefined &&
	  lang.isString(pattern)) {
	opt.file = pattern;
      }
      return comms(opt).then(function(data, ioargs) {
	if (typeof data !== undefined &&
	    typeof data.files !== undefined) {
	  return data.files;
	} else {
	  return undefined;
	}
      });
    };

    return that;

  };

  return rObj;

  });